Fargateから固定IPを使って外部サーバーへ接続する方法を調べてみた
はじめに
データアナリティクス事業本部のkobayashiです。
AWS Fargateを使ってサービスを開発している際に実行しているタスクで外部のサーバーへ接続する必要がありました。ただ外部のサーバーの制限が「固定IPからのみの接続を許可」というものでしたのでFargateのAmazon ECSタスクを実行する際に固定IPアドレスを使う方法を調べたところAWS ナレッジセンターにそのものの記事がありましたので記事を参考にしてTerraformで記述してみました。
NAT ゲートウェイ経由での接続
「Fargate の Amazon ECS タスクに静的 IP アドレスまたは Elastic IP アドレスを使用する 」によると外向きのトラフィックで固定IPを使う場合はNAT ゲートウェイを作成して、NAT ゲートウェイ経由で外部サーバーへ接続します。これによりNAT ゲートウェイのElastic IP アドレスで外部サーバーへ接続することができ、要件を満たすことができます。 構成図を簡単に描くと以下のようになります。
Terafformで記述してみる
上記の構成をTerraformで記述してみます。 Terraformのファイル構成としてはリソース別に以下の様に分けました。
- vpc.tf
- ecs.tf
構築する環境はサブネットCIDRを172.20.0.0/16
で、そのうちPublicサブネットを172.20.1.0/24
、Privateサブネットを172.20.3.0/24
としています。そのPublicサブネットにNAT ゲートウェイを作成します。またPrivateサブネットにFargateタスクを配置するようにしています。
ただし、ECRからイメージを取得をNAT ゲートウェイ経由で行ってしまうとNAT ゲートウェイのデータ転送料金が大きくかかってしまうためVPC エンドポイント経由でECRからイメージを取得するようにしています。この部分に関しては弊社ブログに記事がありますのでそちらをご確認ください。
- vpc.tf
変数部分は適宜環境に合わせて読み替えてください
resource "aws_vpc" "main-vpc" { cidr_block = "172.20.0.0/16" enable_dns_hostnames = true tags = { Name = "${var.aws_project_name}-vpc" } } # Subnet resource "aws_subnet" "public_1a" { cidr_block = "172.20.1.0/24" availability_zone = "${var.aws_region}a" vpc_id = aws_vpc.main-vpc.id tags = { Name = "${var.aws_project_name}-public_1a" } } resource "aws_subnet" "private_1a" { cidr_block = "172.20.3.0/24" availability_zone = "${var.aws_region}a" vpc_id = aws_vpc.main-vpc.id tags = { Name = "${var.aws_project_name}-private_1a" } } # Internet Gateway resource "aws_internet_gateway" "main-igw" { vpc_id = aws_vpc.main-vpc.id tags = { Name = "${var.aws_project_name}-igw" } } # Route Table (Public) resource "aws_route_table" "public" { vpc_id = aws_vpc.main-vpc.id tags = { Name = "${var.aws_project_name}-public" } } # Route resource "aws_route" "public" { destination_cidr_block = "0.0.0.0/0" route_table_id = aws_route_table.public.id gateway_id = aws_internet_gateway.main-igw.id } # Association (Public) resource "aws_route_table_association" "public_1a" { subnet_id = aws_subnet.public_1a.id route_table_id = aws_route_table.public.id } # Route Table (Private) resource "aws_route_table" "private_1a" { vpc_id = aws_vpc.main-vpc.id tags = { Name = "${var.aws_project_name}-private-1a" } } # Association (Private) resource "aws_route_table_association" "private_1a" { subnet_id = aws_subnet.private_1a.id route_table_id = aws_route_table.private_1a.id } # Elasti IP resource "aws_eip" "nat_1a" { vpc = true tags = { Name = "${var.aws_project_name}-natgw-eip-1a" } } # NAT Gateway resource "aws_nat_gateway" "nat_1a" { subnet_id = aws_subnet.public_1a.id allocation_id = aws_eip.nat_1a.id tags = { Name = "${var.aws_project_name}-natgw-1a" } } # VPC Endpoint resource "aws_vpc_endpoint" "s3" { service_name = "com.amazonaws.${var.aws_region}.s3" vpc_endpoint_type = "Gateway" vpc_id = aws_vpc.main-vpc.id route_table_ids = [ aws_route_table.private_1a.id, ] tags = { "Name" = "${var.aws_project_name}-s3" } } resource "aws_vpc_endpoint" "ecr_api" { service_name = "com.amazonaws.${var.aws_region}.ecr.api" vpc_endpoint_type = "Interface" vpc_id = aws_vpc.main-vpc.id subnet_ids = [ aws_subnet.private_1a.id, ] security_group_ids = [ aws_security_group.https.id, ] private_dns_enabled = true tags = { "Name" = "${var.aws_project_name}-ecr-api" } } resource "aws_vpc_endpoint" "ecr_dkr" { service_name = "com.amazonaws.${var.aws_region}.ecr.dkr" vpc_endpoint_type = "Interface" vpc_id = aws_vpc.main-vpc.id subnet_ids = [ aws_subnet.private_1a.id, ] security_group_ids = [ aws_security_group.https.id, ] private_dns_enabled = true tags = { "Name" = "${var.aws_project_name}-ecr-dkr" } } resource "aws_vpc_endpoint" "logs" { service_name = "com.amazonaws.${var.aws_region}.logs" vpc_endpoint_type = "Interface" vpc_id = aws_vpc.main-vpc.id subnet_ids = [ aws_subnet.private_1a.id, ] security_group_ids = [ aws_security_group.https.id, ] private_dns_enabled = true tags = { "Name" = "${var.aws_project_name}-logs" } } resource "aws_vpc_endpoint" "ssm" { service_name = "com.amazonaws.${var.aws_region}.ssm" vpc_endpoint_type = "Interface" vpc_id = aws_vpc.main-vpc.id subnet_ids = [ aws_subnet.private_1a.id, ] security_group_ids = [ aws_security_group.https.id, ] private_dns_enabled = true tags = { "Name" = "${var.aws_project_name}-ssm" } } # SecurityGroup resource "aws_security_group" "https" { name = "https" description = "https" vpc_id = aws_vpc.main-vpc.id egress { cidr_blocks = [aws_vpc.main-vpc.cidr_block] from_port = 443 protocol = "tcp" to_port = 443 } ingress { cidr_blocks = [aws_vpc.main-vpc.cidr_block] from_port = 443 protocol = "tcp" to_port = 443 } tags = { Name = "https" } }
- ecs.tf
変数部分は適宜環境に合わせて読み替えてください。また 実行しているタスクはAPIサーバーをイメージしているのでcontainer_definitions
のportMappings
は80番ポートをマッピングしてあります。こちらも適宜読み替えてください。
# CloudWatch Logs resource "aws_cloudwatch_log_group" "cluster_log_group" { name = "/${var.aws_project_name}/ecs" retention_in_days = 90 } # Task Definition resource "aws_ecs_task_definition" "main" { family = var.aws_project_name requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" execution_role_arn = aws_iam_role.ecs_task_execution_role.arn task_role_arn = aws_iam_role.ecs_task_execution_role.arn cpu = "256" memory = "512" # Container Definitions container_definitions = <<EOL [ { "name": "${var.aws_project_name}", "image": "${var.aws_ecr_repo}", "portMappings": [ { "hostPort": 80, "containerPort": 80 } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-region": "${var.aws_region}", "awslogs-group": "/${var.aws_project_name}/ecs", "awslogs-stream-prefix": "api-server" } } } ] EOL } # task_execution role data "aws_iam_policy_document" "assume_role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } } } resource "aws_iam_role" "ecs_task_execution_role" { name = "${var.aws_project_name}-ecsTaskExecutionRole" assume_role_policy = data.aws_iam_policy_document.assume_role.json } resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" { role = aws_iam_role.ecs_task_execution_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" } # ECS Cluster resource "aws_ecs_cluster" "main" { name = var.aws_project_name } # ECS Service resource "aws_ecs_service" "main" { name = var.aws_project_name cluster = aws_ecs_cluster.main.name launch_type = "FARGATE" desired_count = "1" task_definition = aws_ecs_task_definition.main.arn network_configuration { subnets = [ aws_subnet.private_1a.id, ] security_groups = [aws_security_group.ecs.id] } } # SecurityGroup resource "aws_security_group" "ecs" { name = "${var.aws_project_name}-ecs" description = "${var.aws_project_name} ecs" vpc_id = aws_vpc.main-vpc.id egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${var.aws_project_name}-ecs" } }
まとめ
Fargateから外部サーバーへの接続で固定IPを使用できるようにNAT ゲートウェイを経由するようにしてみました。その際にすべてのトラフィックをNAT ゲートウェイにするとAWSリソースへの接続もNAT ゲートウェイになってしまいNATNAT ゲートウェイの料金が大きく増えてしまうのでAWSリソースへの接続はVPCエンドポイント経由になるようにしました。 なお今回はFargateを使う前提でしたのでNAT ゲートウェイを使いましたが、Fargate使わずにElasticIPを割り当てたEC2タイプのECSを使う方法もあります。